iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
自我挑戰組

賽博紓壓診所:從零打造純前端心理測驗與互動紓壓網頁系列 第 7

Day 07:第一階段總結:打造直覺流暢的溫馨首頁介面

  • 分享至 

  • xImage
  •  

前言
不知不覺,我們的 30 天鐵人賽已經順利走完第一個星期了!

回頭看看前六天,我們從一個單純想幫自己與同儕找出口的念頭出發,一步步用純前端的技術刻出「賽博紓壓診所」的雛形:

  • Day 01 至 Day 02:確立了專案核心,並用語意化標籤建立起四大畫面容器。
  • Day 03:利用 CSS 變數(CSS Variables)與 data-theme 輕鬆搞定夜晚護眼的日夜模式。
  • Day 04:不依賴大型套件,純靠 CSS 的 @keyframes 做出有韻律感的呼吸燈過場。
  • Day 05:透過原生 JavaScript 操作 class,串起不需要重新整理的 SPA 單頁應用架構。
  • Day 06:完成了首頁的互動細節,讓使用者可以從 5 種可愛動物頭像(🐱、🐶、🦊、🐼、🦄)中挑選自己今天的代表心情,並綁定 Email。

今天我們不急著往前推新功能,除了幫第一階段做個小總結之外,特別想花點篇幅來回覆前幾天在留言區看到的一則非常厲害的讀者提問!

讀者 QA 時間:關於畫面切換的架構優化
前幾天文章刊出後,有讀者在底下留言提出了一個很戳中核心的問題:
「用 hidden class 串起四個畫面,對新手來說比一開始就上框架容易理解很多。不過隨著流程變長,分散的切換函式如果漏藏某張卡片,畫面可能會疊在一起。你會考慮寫一個共用的 showScreen(id) 函式來統一保證同一時間只顯示一個畫面,並順便處理焦點嗎?」

看到這則留言的時候真的超感動,謝謝這位讀者願意花時間看文章,還能想到這麼深層的架構問題!這絕對是寫程式寫到一定程度才會有的敏銳度。

單獨寫好幾個分散的切換函式,在畫面少的時候很直覺;但如果專案規模變大、頁面變多,確實很容易發生「忘記把某個畫面藏起來」的小失誤。

如果我們改用一個統一的 showScreen(id) 函式來集中管理,程式碼會變得乾淨很多。概念就像這樣:只要呼叫這個函式,先把所有帶有 .card 的區塊全部加上 hidden 強制隱藏,然後再精準把傳進來的那個 ID 拔掉 hidden 讓它顯示出來。這樣不管切換幾次,都能百分之百保證畫面絕對不會打架!

動手寫 Code:整合第一階段精華與全新 showScreen(id) 架構
為了把讀者這個超棒的建議實踐出來,我們把前六天的成果(日夜模式、呼吸燈、5 種動物頭像選擇、Email 綁定)全部整合在一起,並且把畫面切換邏輯升級成統一的 showScreen(id) 函式:

    [data-theme="dark"] {
        --bg-color: #121212;
        --card-bg: #1e1e1e;
        --primary-color: #64b5f6;
        --primary-hover: #42a5f5;
        --text-main: #e0e0e0;
        --text-muted: #9e9e9e;
    }

    body {
        font-family: 'Segoe UI', Microsoft JhengHei, sans-serif;
        background: var(--bg-color);
        color: var(--text-main);
        margin: 0;
        padding: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 100vh;
        transition: background 0.3s, color 0.3s;
    }

    .container {
        width: 100%;
        max-width: 600px;
        padding: 20px;
    }

    .card {
        background-color: var(--card-bg);
        border-radius: var(--border-radius);
        box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
        padding: 30px;
        text-align: center;
        transition: background-color 0.3s;
    }

    .top-bar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 15px;
        font-size: 0.85rem;
        color: var(--text-muted);
    }

    h1 {
        font-size: 1.5rem;
        color: var(--primary-color);
        margin-bottom: 10px;
    }

    p {
        color: var(--text-muted);
        font-size: 0.95rem;
        margin-bottom: 20px;
    }

    .config-section {
        background: var(--bg-color);
        padding: 15px;
        border-radius: 12px;
        margin-bottom: 20px;
        text-align: left;
        font-size: 0.85rem;
    }

    .config-section label {
        display: block;
        margin-bottom: 5px;
        font-weight: bold;
        color: var(--text-main);
    }

    .config-section input {
        width: 100%;
        padding: 8px;
        border-radius: 6px;
        border: 1px solid #ccc;
        margin-bottom: 12px;
        font-size: 0.9rem;
        background: var(--card-bg);
        color: var(--text-main);
        box-sizing: border-box;
    }

    .avatar-grid {
        display: flex;
        gap: 10px;
        justify-content: center;
        margin-bottom: 12px;
    }

    .avatar-option {
        font-size: 1.8rem;
        cursor: pointer;
        padding: 5px;
        border-radius: 50%;
        border: 2px solid transparent;
        transition: 0.2s;
    }

    .avatar-option.selected {
        border-color: var(--primary-color);
        background-color: var(--bg-color);
        transform: scale(1.1);
    }

    .breathe-circle {
        width: 120px;
        height: 120px;
        background-color: #90caf9;
        border-radius: 50%;
        margin: 30px auto;
        display: flex;
        justify-content: center;
        align-items: center;
        color: white;
        font-weight: bold;
        animation: breatheAnimation 4s infinite ease-in-out;
    }

    @keyframes breatheAnimation {
        0% { transform: scale(1); background-color: #90caf9; }
        50% { transform: scale(1.3); background-color: #42a5f5; }
        100% { transform: scale(1.0); background-color: #90caf9; }
    }

    .btn {
        background-color: var(--primary-color);
        color: white;
        border: none;
        border-radius: 50px;
        padding: 12px 30px;
        font-size: 1rem;
        cursor: pointer;
        width: 100%;
        margin-top: 10px;
        transition: background 0.2s, transform 0.1s;
    }

    .btn:hover {
        background-color: var(--primary-hover);
        transform: translateY(-2px);
    }

    .hidden { display: none; }
</style>
<div class="container">
    
    <!-- 1. 首頁區塊 -->
    <div id="welcome-screen" class="card">
        <div class="top-bar">
            <span>✨ 第一階段總結版:歡迎光臨</span>
            <!-- 修正按鈕文字:此時點下去會切換成亮色 -->
            <button onclick="toggleTheme()" id="theme-btn" style="padding: 4px 10px; border-radius: 6px; cursor: pointer; background: var(--bg-color); color: var(--text-main); border: 1px solid #ccc;">
                ☀️ 亮色
            </button>
        </div>

        <h1>🧠 賽博紓壓診所與解碼器</h1>
        <p>結合廟宇抽籤、六角形能力雷達圖、心電圖暴走與惡搞病歷處方籤的頂級心靈小工具。</p>
        
        <div class="config-section">
            <label>選擇你的今日心情頭像:</label>
            <div class="avatar-grid">
                <div class="avatar-option selected" onclick="selectAvatar('🐱', this)">🐱</div>
                <div class="avatar-option" onclick="selectAvatar('🐶', this)">🐶</div>
                <div class="avatar-option" onclick="selectAvatar('🦊', this)">🦊</div>
                <div class="avatar-option" onclick="selectAvatar('🐼', this)">🐼</div>
                <div class="avatar-option" onclick="selectAvatar('🦄', this)">🦄</div>
            </div>

            <label for="user-email">📧 輸入 Email 記錄長期分析 (選填):</label>
            <input type="email" id="user-email" placeholder="example@email.com (不填則為單次紀錄)">
        </div>

        <button class="btn" onclick="goToBreathe()">進入紓壓診所</button>
    </div>

    <!-- 2. 深呼吸過場區塊(預設隱藏) -->
    <div id="breathe-screen" class="card hidden">
        <h2>🧘 調整呼吸,放鬆心情</h2>
        <p>請跟隨圓縮放的節奏,深吸氣... 吐氣...</p>
        <div id="user-avatar-display" style="font-size: 2.5rem; margin: 10px 0;">🐱</div>
        <div class="breathe-circle" id="breathe-text">吸氣...</div>
        <button class="btn" onclick="startQuiz()">準備好了,開始解碼</button>
    </div>

    <!-- 3. 測驗頁面區塊(預設隱藏) -->
    <div id="quiz-screen" class="card hidden">
        <h2>心理測驗進行中...</h2>
        <p>這裡即將展開我們的 10 道情境題與廟宇抽籤!</p>
        <button class="btn" onclick="showScreen('result-screen')">直接看診斷報告</button>
    </div>

    <!-- 4. 結果報告區塊(預設隱藏) -->
    <div id="result-screen" class="card hidden">
        <h1>🏥 賽博病歷診斷報告</h1>
        <p>今日綜合腦內電池耗損率</p>
        <div class="result-score" id="score-display" style="font-size: 3rem; font-weight: bold; color: var(--primary-color); margin: 10px 0;">0%</div>
        <button class="btn" onclick="showScreen('welcome-screen')">重新掛號測驗</button>
    </div>

</div>

<script>
    let selectedAvatarSymbol = '🐱';
    let userEmailValue = '';

    // 日夜模式切換
    function toggleTheme() {
        const html = document.documentElement;
        const btn = document.getElementById('theme-btn');
        if (html.getAttribute('data-theme') === 'dark') {
            html.removeAttribute('data-theme');
            btn.innerText = "☀️ 亮色";
        } else {
            html.setAttribute('data-theme', 'dark');
            btn.innerText = "🌙 暗色";
        }
    }

    // 頭像選擇
    function selectAvatar(symbol, element) {
        document.querySelectorAll('.avatar-option').forEach(el => el.classList.remove('selected'));
        element.classList.add('selected');
        selectedAvatarSymbol = symbol;
        document.getElementById('user-avatar-display').innerText = symbol;
    }

    // 🌟 統一個畫面切換函式 (確保同一時間只顯示一個畫面)
    function showScreen(screenId) {
        document.querySelectorAll('.card').forEach(card => {
            card.classList.add('hidden');
        });
        document.getElementById(screenId).classList.remove('hidden');
    }

    // 點擊進入過場與呼吸燈計時邏輯
    function goToBreathe() {
        userEmailValue = document.getElementById('user-email').value.trim();
        showScreen('breathe-screen');

        let step = 0;
        const breatheText = document.getElementById('breathe-text');
        
        // 每 2 秒切換一次吸氣/呼氣文字,配合 4 秒的動畫週期
        const interval = setInterval(() => {
            step++;
            if (step === 1) breatheText.innerText = "呼氣...";
            if (step === 2) breatheText.innerText = "吸氣...";
            if (step >= 4) clearInterval(interval);
        }, 2000);
    }

    // 進入正式測驗
    function startQuiz() {
        showScreen('quiz-screen');
    }
</script>

另外在今天的完整程式碼中,我順便加了一些有趣的小文字點綴,也修正了一個不小心寫反的小錯誤——原本切換按鈕文字寫成「🌙 暗色」,但點下去其實是切換成亮色模式,這裡已經把它修正為正確的「☀️ 亮色」囉!

今日結語與明日預告
透過今天的總結與讀者交流,我們不僅把前七天的成果完美揉合成一個結構更漂亮的完整檔案,還順便修正了按鈕小bug、加入了流暢的畫面狀態切換函式!這種「與讀者切磋、互相成長」的感覺真的是寫技術文章最棒的收穫之一。

明天 Day 08:告別沈悶按鈕!用 Web Audio API 動態合成輕柔觸感音效,我們將正式跨入第二階段,不再只有視覺上的互動,還要讓點擊按鈕時發出療癒的清脆音效!我們明天見!


上一篇
Day 06:使用者頭像與設定表單的資料狀態綁定
系列文
賽博紓壓診所:從零打造純前端心理測驗與互動紓壓網頁7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言